/**
 * Copyright (c) 2013 Nokia Corporation. All rights reserved.
 * Nokia and Nokia Connecting People are registered trademarks of Nokia Corporation. 
 * Oracle and Java are trademarks or registered trademarks of Oracle and/or its
 * affiliates. Other product and company names mentioned herein may be trademarks
 * or trade names of their respective owners. 
 * See LICENSE.TXT for license information.
 */

package com.nokia.example.composedui.utils;

import java.io.IOException;
import java.io.InputStream;
import java.lang.ref.WeakReference;
import java.util.Hashtable;

import javax.microedition.io.Connector;
import javax.microedition.io.file.FileConnection;
import javax.microedition.lcdui.Image;

/**
 * Helper class for loading images and scaling them to requested size.
 * The class caches the scaled images for performance.
 */
public class ImageUtils {
    // Constants
    public static final int DO_NOT_SCALE = -1;
    private static final String FILE_SYSTEM_URI_PREFIX = "file://";

    // Members
    private static ImageUtils instance;
    private static Hashtable cache;
    private static int requestId;

    /**
     * Static constructor.
     */
    static {
        cache = new Hashtable();
    }

    /**
     * Constructor.
     */
    private ImageUtils() {
    }

    /** 
     * @return The singleton instance of this class.
     */
    private static ImageUtils getInstance() {
        if (instance == null) {
            instance = new ImageUtils();
        }
        
        return instance;
    }

    /**
     * Gets the image from the cache (or from the original URI if not in cache)
     * asynchronously. Unlike getImage() this method is capable of loading
     * images from files in the file system.
     * @param uri The image URI.
     * @param width The desired image width.
     * @param height The desired image height.
     * @param listener The listener who requested the image.
     * @return The request ID for this particular request.
     */
    public static synchronized int getImageAsync(final String uri,
                                                 final int width,
                                                 final int height,
                                                 final Listener listener)
    {
        final int currentId = requestId;
        
        new Thread() {
            public void run() {
                Image image = null;
                
                if (uri.startsWith(FILE_SYSTEM_URI_PREFIX)) {
                    image = getInstance().loadImageFromFile(uri);
                }
                else {
                    image = getImage(uri, width, height);
                }
                
                if (listener != null) {
                    if (image == null) {
                        listener.onGettingImageFailed();
                    }
                    else {
                        if ((width > 0  && height > 0)
                            && (image.getWidth() != width || image.getHeight() != height))
                        {
                            image = pixelMixingScale(image, width, height);
                        }
                        
                        // Store the image in the cache
                        final String key = createKey(uri, width, height);
                        WeakReference ref = new WeakReference(image);
                        cache.put(key, ref);
                        
                        // Notify the listener
                        listener.onImageReady(currentId, image);
                    }
                }
            }
        }.start();
        
        return requestId++;
    }

    /**
     * Returns a scaled image from a given location. The scaled image is cached
     * for later use.
     * @param The image URI.
     * @param width The desired image width.
     * @param height The desired image height.
     * @return A scaled image or null in case of a failure.
     */
    public static synchronized Image getImage(final String uri,
                                              final int width,
                                              final int height)
    {
        // Fetch a scaled image from cache
        final String key = createKey(uri, width, height);
        WeakReference ref = (WeakReference) cache.get(key);
        
        if (ref != null) {
            // Check if the WeakReference is still valid
            Object o = ref.get();
            
            if (o != null) {
                return (Image) o;
            }
        }
        
        // Image wasn't in cache or the WeakReference was invalid. Create a new
        // scaled Image and store it into cache. Use ImageLoader to load image
        // from either resource or phone.
        Image image = getInstance().loadImage(uri);
        
        if (image != null) {
            if ((width > 0  && height > 0)
                && (image.getWidth() != width || image.getHeight() != height))
            {
                image = pixelMixingScale(image, width, height);
            }
            
            // Store the image in the cache
            ref = new WeakReference(image);
            cache.put(key, ref);
        }
        
        return image;
    }

    /**
     * For convenience.
     */
    public static Image getImage(final String uri) {
        return getImage(uri, DO_NOT_SCALE, DO_NOT_SCALE);
    }

    /**
     * Loads an image from the given URI returns it.
     * @param imageUri The image URI.
     * @return The loaded image.
     */
    public final synchronized Image loadImage(final String imageUri)
    {
        Image image = null;
        
        try {
            image = Image.createImage(imageUri);
        }
        catch (IOException e) {
            image = null;
        }
        
        return image;
    }

    /**
     * Loads an image from a file in the file system and returns it.
     * @param imageUri The image URI.
     * @return The loaded image. 
     */
    public final Image loadImageFromFile(final String imageUri)
    {
        System.out.println("ImageUtils.loadImageFromFile(): " + imageUri);
        Image image = null;
        
        try {
            FileConnection fileConnection = (FileConnection)Connector.open(imageUri);
            InputStream in = fileConnection.openInputStream();
            
            if (in != null) {
                image = Image.createImage(in);
                in.close();
            }
            else {
                System.out.println("ImageUtils.loadImageFromFile(): Failed to open the input stream!");
            }
            
            fileConnection.close();
        }
        catch (IOException e) {
            e.printStackTrace();
        }
        catch (IllegalArgumentException e) {
            e.printStackTrace();
        }
        catch (SecurityException e) {
            System.out.println("ImageUtils.loadImageFromFile(): User has not given permission to access the file system!");
        }
        
        return image;
    }

    /**
     * Scales the given image. Note that this is a very simple scaling
     * algorithm that may result in poor quality. In addition, the alpha
     * channel is lost.
     * @param sourceImage The image to scale.
     * @param newWidth The new, target width of the image.
     * @param newHeight The new, target height of the image.
     * @return A scaled image.
     */
    public static final synchronized Image simpleScale(
        final Image sourceImage,
        final int newWidth,
        final int newHeight)
    {
        // Get the original dimensions of the image
        final int srcWidth = sourceImage.getWidth();
        final int srcHeight = sourceImage.getHeight();
        
        // An array for RGB, with the size of original image)
        int rgbSource[] = new int[srcWidth * srcHeight];
        
        // An array for RGB, with the size of scaled image)
        int rgb2Scaled[] = new int[newWidth * newHeight];
        
        // Get the RGB array of source image
        sourceImage.getRGB(rgbSource, 0, srcWidth, 0, 0, srcWidth, srcHeight);
        
        // Calculations and bit shift operations to optimize the for loop
        int tempScaleRatioWidth = ((srcWidth << 16) / newWidth);
        int tempScaleRatioHeight = ((srcHeight << 16) / newHeight);
        
        int i = 0;
        
        for (int y = 0; y < newHeight; y++) {
            for (int x = 0; x < newWidth; x++) {
                rgb2Scaled[i++] = rgbSource[(srcWidth *
                          ((y * tempScaleRatioHeight) >> 16))
                        + ((x * tempScaleRatioWidth) >> 16)];
            }
        }
        
        // Create an RGB image from rgbScaled array
        return Image.createRGBImage(rgb2Scaled, newWidth, newHeight, false);
    }

    /**
     * Scales the given image using a pixel mixing algorithm.
     * @param originalImage
     * @param newWidth The new, target width of the image.
     * @param newHeight The new, target height of the image.
     * @return A scaled image.
     * @throws IllegalArgumentException
     */
    public static final synchronized Image pixelMixingScale(
        final Image originalImage,
        final int newWidth,
        final int newHeight)
            throws IllegalArgumentException
    {
        if (newWidth <= 0 || newHeight <= 0 || originalImage == null) {
            throw new IllegalArgumentException(
                "Invalid width or height or the given image is null!");
        }
        
        final int sourceWidth = originalImage.getWidth();
        final int sourceHeight = originalImage.getHeight();
        final int[] originalRgb = new int[sourceHeight * sourceWidth];
        originalImage.getRGB(originalRgb, 0, sourceWidth, 0, 0, sourceWidth, sourceHeight);
        
        final int[] scaledRgb = new int[newWidth * newHeight];
        
        final int oWidth = sourceWidth;
        final int[] oX16 = new int[newWidth + 1];
        
        for (int newX = 0; newX <= newWidth; newX++) {
            oX16[newX] = ((newX * oWidth) << 4) / newWidth;
        }
        
        final int[] oXStartWidth = new int[newWidth];
        final int[] oXEndWidth = new int[newWidth];
        
        for (int newX = 0; newX < newWidth; newX++) {
            oXStartWidth[newX] = 16 - (oX16[newX] % 16);
            oXEndWidth[newX] = oX16[newX + 1] % 16;
        }
        
        final int oHeight = sourceHeight;
        final int[] oY16 = new int[newHeight + 1];
        
        for (int newY = 0; newY <= newHeight; newY++) {
            oY16[newY] = ((newY * oHeight) << 4) / newHeight;
        }
        
        for (int newY = 0; newY < newHeight; newY++) {
            final int oY16Start = oY16[newY];
            final int oY16End = oY16[newY + 1];
            final int oYStart = oY16Start >>> 4;
            final int oYEnd = oY16End >>> 4;
            final int oYStartHeight = 16 - (oY16Start % 16);
            final int oYEndHeight = oY16End % 16;
            
            for (int newX = 0; newX < newWidth; newX++) {
                final int oX16Start = oX16[newX];
                final int oX16End = oX16[newX + 1];
                final int oXStart = oX16Start >>> 4;
                final int oXEnd = oX16End >>> 4;
                int outArea = 0;
                int outColorArea = 0;
                int outAlpha = 0;
                int outRed = 0;
                int outGreen = 0;
                int outBlue = 0;
                
                for (int j = oYStart; j <= oYEnd; j++) {
                    final int areaHeight;
                    
                    if (oYStart == oYEnd) {
                        areaHeight = oY16End - oY16Start;
                    }
                    else if (j == oYStart) {
                        areaHeight = oYStartHeight;
                    }
                    else if (j == oYEnd) {
                        areaHeight = oYEndHeight;
                    }
                    else {
                        areaHeight = 16;
                    }
                    
                    if (areaHeight == 0) {
                        continue;
                    }
                    
                    for (int i = oXStart; i <= oXEnd; i++) {
                        final int areaWidth;
                        
                        if (oXStart == oXEnd) {
                            areaWidth = oX16End - oX16Start;
                        }
                        else if (i == oXStart) {
                            areaWidth = oXStartWidth[newX];
                        }
                        else if (i == oXEnd) {
                            areaWidth = oXEndWidth[newX];
                        }
                        else {
                            areaWidth = 16;
                        }
                        
                        if (areaWidth == 0) {
                            continue;
                        }
                        
                        int area = areaWidth * areaHeight;
                        outArea += area;
                        int argb = originalRgb[i + j * sourceWidth];
                        int a = (argb >>> 24);
                        
                        if (a == 0) {
                            continue;
                        }
                        
                        area = a * area;
                        outColorArea += area;
                        final int r = (argb & 0x00ff0000) >>> 16;
                        final int g = (argb & 0x0000ff00) >>> 8;
                        final int b = argb & 0x000000ff;
                        outRed += area * r;
                        outGreen += area * g;
                        outBlue += area * b;
                    }
                }
                
                if (outColorArea > 0) {
                    outAlpha = outColorArea / outArea;
                    outRed = outRed / outColorArea;
                    outGreen = outGreen / outColorArea;
                    outBlue = outBlue / outColorArea;
                }
                
                scaledRgb[newX + newY * newWidth] = (outAlpha << 24)
                    | (outRed << 16) | (outGreen << 8) | outBlue;
            }
        }
        
        return Image.createRGBImage(scaledRgb, newWidth, newHeight, true);
    }

    /**
     * Generates a unique key for the image of requested size.
     * @param uri The image URI.
     * @param width The desired image width.
     * @param height The desired image height.
     * @return The unique key as a string.
     */
    private static String createKey(final String uri,
                             final int width,
                             final int height)
    {
        return uri + width + "x" + height;
    }

    /**
     * Listener interface for classes requesting images from cache
     * asynchronously.
     */
    public interface Listener {
        /**
         * @param requestId The ID associated with the request.
         * @param image The image requested earlier.
         */
        void onImageReady(int requestId, Image image);
        
        /**
         * Called if the image could not be loaded.
         */
        void onGettingImageFailed();
    }
}
